package com.douma.service.impl;

import com.alibaba.fastjson.JSON;
import com.douma.dao.CommentDao;
import com.douma.dao.ShopDao;
import com.douma.dto.Comment;
import com.douma.dto.Shop;
import com.douma.service.ShopService;
import com.douma.utils.RedisConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


/**
 * 抖码课堂，专注中国程序员基础内功修炼
 * 官网：douma-tech.com
 * 公众号：抖码课堂
 *
 * @作者 : 老汤
 */

@Service("ShopServiceImpl_logic_expire")
public class ShopServiceImpl_缓存击穿_逻辑过期 implements ShopService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ShopDao shopDao;

    @Autowired
    private CommentDao commentDao;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private Random random = new Random();

    private ExecutorService CACHE_REBUILD_THREAD_POOL = Executors.newFixedThreadPool(10);

    @Override
    public Shop findById(Long id) {
        logger.info("开始【业务】查询商铺，商铺 id 为：" + id);
        String key = RedisConstants.SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (shopJson == null || shopJson.isEmpty()) return null;

        // 判断是否过期
        Shop shop = JSON.parseObject(shopJson, Shop.class);
        LocalDateTime expireTime = shop.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 没有过期，直接返回
            return shop;
        }
        // 已经过期，需要重建缓存
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        if (tryLock(lockKey)) {
            Shop cacheShop = findShopFromCache(key);
            if (cacheShop != null &&
                    cacheShop.getExpireTime().isAfter(LocalDateTime.now())) {
                return cacheShop;
            }
            // 获取锁成功，开启独立线程，执行缓存重建
            CACHE_REBUILD_THREAD_POOL.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Shop shop = shopDao.queryById(id);
                        shop.setExpireTime(LocalDateTime.now().plusSeconds(RedisConstants.SHOP_LOGIC_EXPIRE_TIME));
                        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(shop));
                    } finally {
                        unlock(lockKey);
                    }
                }
            });
        }
        // 加锁不成功，返回过期的 shop
        return shop;
    }

    private Shop findShopFromCache(String key) {
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (shopJson != null && !shopJson.isEmpty()) {
            logger.info("【业务】查询商铺结束，从缓存 Redis 中返回数据");
            return JSON.parseObject(shopJson, Shop.class);
        }
        return null;
    }

    /**
     * 尝试获取锁
     * @param key 锁对应的 key
     * @return  执行 SETNX 成功，则表示获取锁，返回 true，否则返回 false
     */
    private boolean tryLock(String key) {
        Boolean succ = stringRedisTemplate.opsForValue()
                .setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return succ == null ? false : succ;
    }

    /**
     * 释放锁
     * @param key
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }


    @Override
    @Transactional
    public void comment(Comment comment) {
        // 1. 更新 MySQL
        // 1.1. 将商铺表中的评论数增加 1
        shopDao.incCommentsById(comment.getShopId());
        // 1.2. 将评论插入到评论表中
        commentDao.insert(comment);

        // 2. 后删除缓存
        String redisKey = RedisConstants.SHOP_KEY + comment.getShopId();
        stringRedisTemplate.delete(redisKey);
    }

    // 延迟双删  --> 数据不一致问题
    public void comment3(Comment comment) {
        // 1. 先删除缓存
        String redisKey = RedisConstants.SHOP_KEY + comment.getShopId();
        stringRedisTemplate.delete(redisKey);

        // 2. 更新 MySQL
        // 2.1. 将商铺表中的评论数增加 1
        shopDao.incCommentsById(comment.getShopId());
        // 2.2. 将评论插入到评论表中
        commentDao.insert(comment);

        // 延迟
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 3. 再次删除 Redis 缓存
        stringRedisTemplate.delete(redisKey);
    }

    // 并发双写 --》 数据不一致问题
    public void comment2(Comment comment) {
        // 1. 更新 MySQL
        // 1.1. 将商铺表中的评论数增加 1
        shopDao.incCommentsById(comment.getShopId());
        // 1.2. 将评论插入到评论表中
        commentDao.insert(comment);

        // 2. 更新 Redis 缓存
        // value 是 string 类型的 json 的更新逻辑
        String redisKey = RedisConstants.SHOP_KEY + comment.getShopId();
        String shopJson = stringRedisTemplate.opsForValue().get(redisKey);
        if (shopJson != null && !shopJson.isEmpty()) {
            Shop shop = JSON.parseObject(shopJson, Shop.class);
            shop.setComments(shop.getComments() + 1);
            stringRedisTemplate.opsForValue()
                    .set(redisKey, JSON.toJSONString(shop));
        }
    }

    @Override
    public Long addShop(Shop shop) {
        // 1. 将商铺数据插入到数据库，并返回商铺的 id
        Long shopId = shopDao.insert(shop);
        // 2. 把新增的 id 放到布隆过滤器
        // rBloomFilter.add(shopId);
        return shopId;
    }

    /*@Override
    public void afterPropertiesSet() throws Exception {
        // 在设置当前这个类的属性值的时候，会执行
        logger.info("【开始执行】同步所有的商铺 id ");
        // 1. 查询所有的商铺 id
        List<Long> shopIds = shopDao.queryAllIds();

        // 2. 将商铺 id 设置到布隆过滤器
        shopIds.forEach(id -> bloomFilter.put(id));
        logger.info("【结束】同步所有的商铺 id ");
    }*/
}